package com.dmj.sqldsl.executor.visitor;

import static java.util.stream.Collectors.joining;

import com.dmj.sqldsl.executor.exception.UnsupportedJoinFlagException;
import com.dmj.sqldsl.model.DslQuery;
import com.dmj.sqldsl.model.GroupBy;
import com.dmj.sqldsl.model.Having;
import com.dmj.sqldsl.model.Join;
import com.dmj.sqldsl.model.JoinFlag;
import com.dmj.sqldsl.model.Limit;
import com.dmj.sqldsl.model.OrderBy;
import com.dmj.sqldsl.model.SelectFrom;
import com.dmj.sqldsl.model.column.SimpleColumn;
import com.dmj.sqldsl.model.column.SubQueryValueColumn;
import com.dmj.sqldsl.model.table.SimpleTable;
import com.dmj.sqldsl.model.table.SubQueryTable;
import java.util.HashMap;
import java.util.Map;

public class QueryModelVisitor extends ModelVisitor {

  private int aliasIndex = 0;

  private final Map<String, String> realAliasMap = new HashMap<>();

  public String visit(DslQuery query) {
    String selectFromWhere = visitSelectFromWhere(query);
    String orderBy = query.getOrderBy().map(this::visit).orElse("");
    selectFromWhere = selectFromWhere + orderBy;
    String finalSelectFromWhere = selectFromWhere;
    return query.getLimit()
        .map(limit -> visitLimit(finalSelectFromWhere, limit))
        .orElse(selectFromWhere);
  }

  @Override
  protected String visit(SubQueryValueColumn column) {
    return String.format("(%s)", visit(column.getQuery()));
  }

  protected String visit(Limit limit) {
    params.add(new Parameter(limit.getOffset()));
    params.add(new Parameter(limit.getSize()));
    return " limit ?,?";
  }

  protected String visit(Join join) {
    return String.format("%s %s on %s", visit(join.getFlag()),
        visit(join.getTable()), visit(join.getConditions()));
  }

  protected String visit(JoinFlag flag) {
    switch (flag) {
      case left:
        return "left join";
      case right:
        return "right join";
      case inner:
        return "inner join";
      default:
        throw new UnsupportedJoinFlagException(flag);
    }
  }

  protected String visit(SelectFrom selectFrom) {
    String columnsSqlString = selectFrom.getColumns().stream()
        .map(this::visit)
        .collect(joining(", "));
    String tablesSqlString = visit(selectFrom.getTable());
    String joinSqlString = selectFrom.getJoins()
        .map(x -> x.stream().map(this::visit).collect(joining(" ", " ", "")))
        .orElse("");
    return String.format("select %s from %s%s", columnsSqlString, tablesSqlString, joinSqlString);
  }

  protected String visit(SimpleColumn column) {
    return column.getTableId()
        .map(tableId -> String.format("%s.%s", getRealAlias(tableId), column.getColumnName()))
        .orElse(column.getColumnName());
  }

  @Override
  protected String visit(SimpleTable table) {
    return String.format("%s as %s", table.getTableName(), getRealAlias(table.getTableId()));
  }

  @Override
  protected String visit(SubQueryTable table) {
    return String.format("(%s) as %s", visit(table.getQuery()), getRealAlias(table.getTableId()));
  }

  protected String visit(OrderBy orderBy) {
    String orders = orderBy.getOrders().stream()
        .map(order ->
            String.format("%s %s", visit(order.getColumn()), order.isAsc() ? "asc" : "desc"))
        .collect(joining(","));
    return String.format(" order by %s", orders);
  }

  protected String visit(GroupBy groupBy) {
    String columnsSql = groupBy.getColumns().stream()
        .map(this::visit)
        .collect(joining(","));
    return String.format(" group by %s", columnsSql);
  }

  protected String visit(Having having) {
    return String.format(" having %s", visit(having.getConditions()));
  }

  protected String visitLimit(String allSqlWithoutLimit, Limit limit) {
    return allSqlWithoutLimit + visit(limit);
  }

  protected String visitSelectFromWhere(DslQuery query) {
    String selectFrom = visit(query.getSelectFrom());
    String where = query.getWhere().map(this::visit).orElse("");
    String groupBy = query.getGroupBy().map(this::visit).orElse("");
    String having = query.getHaving().map(this::visit).orElse("");
    return selectFrom + where + groupBy + having;
  }

  protected String getRealAlias(String tableName) {
    return realAliasMap.computeIfAbsent(tableName,
        name -> "t" + aliasIndex++);
  }
}
